Mapper Container topic

MapperContainers are the backbone of dart_mappable. A MapperContainers job is to lookup the correct mapper for a type and call its respective method.

To find the mapper for a given type, the container first looks at its own set of mappers and when there is no match it refers to its linked containers.


🚨 Important!

A lot of this may be removed in a future version. See https://github.com/schultek/dart_mappable/issues/159 for more details.


Working with MapperContainers

This is an advanced topic. Usually you don't need to worry about containers, as they are hidden from the user when doing standard things like decoding or encoding from json or using the generated mixins toString, == or hashCode overrides.

You don't need to know this for most use-cases. But feel free to read on if you are interested.

By default there is only the globally accessible MapperContainer.globals. The interface of a container contains all the respective methods for serialization and mapping, like fromJson, toJson, isEqual, hash and asString. You can use these methods with any object as long as the container can resolve a mapper for it.

To control which types a MapperContainer can resolve see the following methods:

  • use<T>(MapperBase<T> mapper) will add a new mapper for type T.

  • unuse<T>() will remove the current mapper for type T. Note that this works for any mapper, custom, generated or for primitive types.

  • useAll(Iterable<MapperBase> mapper) works as use() but for a list of mappers.

  • get<T>() will return the current mapper for type T.

  • getAll() will return all currently registered mappers.

The MapperContainer.global has a default set of mappers for all core Dart types. When calling MyClassMapper.ensureInitialized() on a generated mapper, it will add itself to the global container and from thereon be usable through it.

Additionally to using the preconfigured containers, you can also create one yourself using the MapperContainer() constructor.

In extension to controlling mappers manually for a single container, you can also link a container to another. That way container A can use all mappers of container B implicitly.

  • link(MapperContainer container) links the provided container to the current one.

The following example shows this in action for a schematic setup of two classes and mappers.


// Suppose we have two classes and their respective mappers
class A {}
class B {}

class AMapper extends MapperBase<A> {} // generated
class BMapper extends MapperBase<B> {} // generated

void main() {
  
  AMapper.ensureInitialized();
  
  // succeeds - AMapper was added to the globals container.
  MapperContainer.globals.toJson(A());
  
  // fails - B is not initialized yet.
  MapperContainer.globals.toJson(A());

  BMapper.ensureInitialized();
  
  // creates a new empty container
  var containerX = MapperContainer();

  // fails - no mappers were added yet
  containerX.toJson(A());

  // succeeds - added the respective mapper
  containerX.use(AMapper());
  containerX.toJson(A());
  
  var containerY = MapperContainer();
  containerY.use(BMapper());

  // succeeds - added the respective mapper
  containerY.toJson(B());
  // fails - missing mapper for type A
  containerY.toJson(A());
  
  // links containerX to containerY
  containerY.link(containerX);

  // succeeds - mapper is resolved through linked containerX
  containerY.toJson(A());
  
  // fails - linking is one-directional
  containerX.toJson(B());
}

Defaults Container

There is a special global defaults container that can be accessed using MapperContainer.defaults. This container initially contains mappers for all primitive and core types. Types included are:

dynamic, Object, String, int, double, num, bool, DateTime, List, Set, Map

All other containers are implicitly linked to this default container.

You can use this container to add mappers for types, that are thereby supported by all other containers.

// suppose we have a class and a custom mapper for it
class MyClass {}
class MyClassMapper extends SimpleMapper<MyClass> { ... }

void main() {
  
  var container = MapperContainer();
  // this doen't work, since the mapper for type [MyClass] wasn't added yet to this container
  container.toJson(MyClass());
  
  // we add the mapper as a global default
  MapperContainer.defaults.use(MyClassMapper());

  // now this works, since all containers are linked to the default container
  container.toJson(MyClass());
}

Globals Container

The second special container is the MapperContainer.globals container. This container is used by all generated mappers for global registration.

A generated mapper adds itself to the globals container when calling MyMapper.ensureInitialized().

Encoding Lists, Sets and Maps

Because all of the MapperContainers methods are generic, you are not limited to use a single object with these methods. dart_mappable also support Lists, Sets and Maps out of the box, without any special syntax, workarounds or hacks.

@MappableClass()
class Dog with DogMappable {
  String name;
  Dog(this.name);
}

@MappableClass()
class Box<T> with BoxMappable<T> {
  T content;
  Box(this.content);
}

void main() {
  // Case 1: Simple list.
  // We use the default container since we only use core types.
  List<int> nums = MapperContainer.globals.fromJson('[2, 4, 105]');
  print(nums); // [2, 4, 105]

  // Case 2: Set of objects.
  // We use the generated container for [Dog], but with a set of objects.
  DogMapper.ensureInitialized();
  Set<Dog> dogs = MapperContainer.globals.fromJson('[{"name": "Thor"}, {"name": "Lasse"}, {"name": "Thor"}]');
  print(dogs); // {Dog(name: Thor), Dog(name: Lasse)}

  // Case 3: More complex lists, like generics.
  // We use the generated container for [Box], but with a list of generic objects.
  BoxMapper.ensureInitialized();
  List<Box<double>> boxes = MapperContainer.globals.fromJson('[{"content": 0.1}, {"content": 12.34}]');
  print(boxes); // [Box(content: 0.1), Box(content: 12.34)]
}

There is also the MapperContainer.fromIterable method. This can be used if you already have a list of dynamic objects instead of the raw json string. Additionally this can get handy to decode a dynamic list of partly-encoded values:

List<double> myNumbers = MapperContainer.globals.fromIterable([2.312, '1.32', 500, '1e4']);
print(myNumbers); // [2.312, 1.32, 500.0, 10000.0]

Non-Trivial Maps

We also support decoding of non-trivial maps. Although the use-cases might be rare, you can decode to something other than a map of string keys like this:

var encodedMap = {
  {'name': 'Bonny'}: 1,
  {'name': 'Clyde'}: 5,
};

DogMapper.ensureInitialized();
Map<Dog, int> treatsPerDog = MapperContainer.globals.fromValue(encodedMap);
print(treatsPerDog[Dog('Clyde')]!); // 5

var myMap = MapperContainer.globals.toValue(treatsPerDog);
print(myMap); // {{name: Bonny}: 1, {name: Clyde}: 5}

Note: Make sure to add @MappableClass on your key class, in order to enable easy property access.

Note: Since json only supports string keys, we can't do fromJson or toJson on these maps. You would have to decode / encode your keys and values separately.


🎉 Congrats, you followed the tour until the end. Now you know everything about this package.

Classes

DecodingOptions Generics Mapper Container
Additional options to be passed to MapperContainer.fromValue.
EncodingOptions Generics Mapper Container
Additional options to be passed to MapperContainer.toValue.
MapperContainer Generics Mapper Container
The mapper container manages a set of mappers and is the main interface for accessing mapping functionalities.

Exceptions / Errors

MapperException Mapper Container
General exception class used throughout the package.